// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xml.impl;

import com.intellij.javaee.ExternalResourceManagerEx;
import com.intellij.lang.dtd.DTDLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.impl.source.html.dtd.HtmlNSDescriptorImpl;
import com.intellij.psi.meta.PsiMetaOwner;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.xml.XmlDoctype;
import com.intellij.psi.xml.XmlDocument;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlProlog;
import com.intellij.util.AstLoadingFilter;
import com.intellij.xml.Html5SchemaProvider;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.index.XmlNamespaceIndex;
import com.intellij.xml.util.XmlNSDescriptorSequence;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class XmlNsDescriptorUtil {

  private static final Logger LOG = Logger.getInstance(XmlNsDescriptorUtil.class);
  private static final Key<Boolean> AUTO_GENERATED = Key.create("auto-generated xml schema");

  public static boolean isAutoGeneratedSchema(XmlFile file) {
    return file.getUserData(AUTO_GENERATED) != null;
  }

  public static @Nullable XmlNSDescriptor getCachedHtmlNsDescriptor(final @NotNull XmlFile descriptorFile) {
    return getCachedHtmlNsDescriptor(descriptorFile, "");
  }

  public static @Nullable XmlNSDescriptor getCachedHtmlNsDescriptor(final @NotNull XmlFile descriptorFile, final @NotNull String prefix) {
    Map<String, XmlNSDescriptor> descriptorsByPrefix = CachedValuesManager.getCachedValue(descriptorFile, () -> {
      return CachedValueProvider.Result.create(new ConcurrentHashMap<>(), descriptorFile);
    });
    return descriptorsByPrefix.computeIfAbsent(prefix, p -> {
      final XmlDocument document = descriptorFile.getDocument();
      if (document == null) return null;
      return new HtmlNSDescriptorImpl((XmlNSDescriptor)document.getMetaData());
    });
  }

  public static boolean isGeneratedFromDtd(final @NotNull XmlDocument document,
                                           XmlNSDescriptor defaultNSDescriptorInner) {
    if (defaultNSDescriptorInner == null) {
      return false;
    }
    XmlFile descriptorFile = defaultNSDescriptorInner.getDescriptorFile();
    if (descriptorFile == null) {
      return false;
    }
    @NonNls String otherName = XmlUtil.getContainingFile(document).getName() + ".dtd";
    return descriptorFile.getName().equals(otherName);
  }

  public static @Nullable XmlNSDescriptor getDefaultNSDescriptor(final @NotNull XmlDocument document,
                                                                 final @Nullable String namespace,
                                                                 final boolean strict) {
    final XmlFile containingFile = XmlUtil.getContainingFile(document);
    if (containingFile == null) return null;
    final XmlProlog prolog = document.getProlog();
    final XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null;
    boolean dtdUriFromDocTypeIsNamespace = false;

    if (XmlUtil.HTML_URI.equals(namespace)) {
      XmlNSDescriptor nsDescriptor = doctype != null ? getNsDescriptorFormDocType(document, doctype, containingFile, true) : null;
      if (doctype != null) {
        LOG.debug(
          "Descriptor from doctype " + doctype + " is " + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL"));
      }

      if (nsDescriptor == null) {
        String htmlns = ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(document.getProject());
        if (htmlns.isEmpty()) {
          htmlns = Html5SchemaProvider.getHtml5SchemaLocation();
        }
        nsDescriptor = getDefaultNSDescriptor(document, htmlns, false);
      }
      if (nsDescriptor != null) {
        final XmlFile descriptorFile = nsDescriptor.getDescriptorFile();
        if (descriptorFile != null) {
          return getCachedHtmlNsDescriptor(descriptorFile);
        }
      }
      return new HtmlNSDescriptorImpl(nsDescriptor);
    }
    else if (XmlUtil.XHTML_URI.equals(namespace)) {
      String xhtmlNamespace = XmlUtil.getDefaultXhtmlNamespace(document.getProject());
      if (xhtmlNamespace == null || xhtmlNamespace.isEmpty()) {
        xhtmlNamespace = Html5SchemaProvider.getXhtml5SchemaLocation();
      }
      return getDefaultNSDescriptor(document, xhtmlNamespace, false);
    }
    else if (namespace != null && !namespace.equals(XmlUtil.EMPTY_URI)) {
      if (doctype == null || !namespace.equals(XmlUtil.getDtdUri(doctype))) {
        boolean documentIsSchemaThatDefinesNs = namespace.equals(XmlUtil.getTargetSchemaNsFromTag(document.getRootTag()));

        final XmlFile xmlFile = documentIsSchemaThatDefinesNs
                                ? containingFile
                                : XmlUtil.findNamespace(containingFile, namespace);
        if (xmlFile != null) {
          final XmlDocument nsDocument = AstLoadingFilter.forceAllowTreeLoading(xmlFile, () -> xmlFile.getDocument());
          if (nsDocument != null) {
            return (XmlNSDescriptor)nsDocument.getMetaData();
          }
        }
      }
      else {
        dtdUriFromDocTypeIsNamespace = true;
      }
    }

    if (strict && !dtdUriFromDocTypeIsNamespace) return null;

    if (doctype != null) {
      XmlNSDescriptor descr = getNsDescriptorFormDocType(document, doctype, containingFile, false);

      if (descr != null) {
        return XmlExtension.getExtension(containingFile).getDescriptorFromDoctype(containingFile, descr);
      }
    }

    if (strict) return null;
    if (XmlUtil.EMPTY_URI.equals(namespace)) {
      final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, namespace);
      if (xmlFile != null) {
        return (XmlNSDescriptor)xmlFile.getDocument().getMetaData();
      }
    }
    try {
      final PsiFile fileFromText = PsiFileFactory.getInstance(document.getProject())
        .createFileFromText(containingFile.getName() + ".dtd", DTDLanguage.INSTANCE, XmlUtil.generateDocumentDTD(document, false), false,
                            false);
      if (fileFromText instanceof XmlFile) {
        fileFromText.putUserData(AUTO_GENERATED, Boolean.TRUE);
        return (XmlNSDescriptor)((XmlFile)fileFromText).getDocument().getMetaData();
      }
    }
    catch (ProcessCanceledException ex) {
      throw ex;
    }
    catch (RuntimeException ignored) {
    } // e.g. dtd isn't mapped to xml type

    return null;
  }

  private static @Nullable XmlNSDescriptor getNsDescriptorFormDocType(
    @NotNull XmlDocument document, final XmlDoctype doctype, final XmlFile containingFile, final boolean forHtml
  ) {
    XmlNSDescriptor descriptor = getNSDescriptorFromMetaData(document, doctype.getMarkupDecl(), true);

    final String filePath = getFilePathForLogging(containingFile);

    final String dtdUri = XmlUtil.getDtdUri(doctype);
    LOG.debug("DTD url for doctype " + doctype.getText() + " in file " + filePath + " is " + dtdUri);

    if (dtdUri != null && !dtdUri.isEmpty()) {
      XmlFile xmlFile = XmlUtil.findNamespace(containingFile, dtdUri);
      if (xmlFile == null) {
        // try to auto-detect it
        xmlFile = XmlNamespaceIndex.guessDtd(dtdUri, containingFile);
      }
      final String schemaFilePath = getFilePathForLogging(xmlFile);

      LOG.debug("Schema file for " + filePath + " is " + schemaFilePath);

      XmlNSDescriptor descriptorFromDtd = getNSDescriptorFromMetaData(document,
                                                                      xmlFile == null
                                                                      ? null
                                                                      : AstLoadingFilter.forceAllowTreeLoading(xmlFile,
                                                                                                               xmlFile::getDocument),
                                                                      forHtml);

      LOG.debug("Descriptor from meta data for schema file " +
                schemaFilePath +
                " is " +
                (descriptorFromDtd != null ? descriptorFromDtd.getClass().getCanonicalName() : "NULL"));

      if (descriptor != null && descriptorFromDtd != null) {
        descriptor = new XmlNSDescriptorSequence(new XmlNSDescriptor[]{descriptor, descriptorFromDtd});
      }
      else if (descriptorFromDtd != null) {
        descriptor = descriptorFromDtd;
      }
    }
    return descriptor;
  }

  private static @Nullable XmlNSDescriptor getNSDescriptorFromMetaData(@NotNull XmlDocument document,
                                                                       @Nullable PsiMetaOwner metaOwner,
                                                                       boolean nonEmpty) {
    if (metaOwner == null) return null;
    XmlNSDescriptor descriptor = (XmlNSDescriptor)metaOwner.getMetaData();
    if (descriptor == null) return null;
    if (nonEmpty && descriptor.getRootElementsDescriptors(document).length == 0) {
      return null;
    }
    return descriptor;
  }

  private static @NotNull String getFilePathForLogging(@Nullable PsiFile file) {
    if (file == null) {
      return "NULL";
    }
    final VirtualFile vFile = file.getVirtualFile();
    return vFile != null ? vFile.getPath() : "NULL_VFILE";
  }
}
